package org.django4j;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.django4j.UrlsContext.RequestMethod;

/**
 * DispatchServlet会处理所有进入的请求，并且将请求的(url+requestMethod)与django4j-urls.properties
 * 中所有url正则表达式逐个进行比较，直到碰到第一个匹配的正则表达式，然后通过这个正则表达式就能
 * 够获得请求对应的controller方法.
 * 比如有一个"http://localhost:8000/user/3/"的get请求， DispatchServlet会取出
 * requestUrl('user/3/'),然后加上request method，就得到'user/3/GET',最后将'user/3/GET'
 * 与 {@link UrlContext}中的正则表单是逐个比较，只取第一个匹配的UrlMapping.
 */
public class DispatchServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private static final String DELIM_URL = "/";
	private final Log logger = LogFactory.getLog(DispatchServlet.class);
	
	private UrlsContext urlsContext = null;
	/** You don't need to modify the value of urlmappingFile */
	private final String urlmappingFile = "WEB-INF/django4j-urls.properties";

	public void init() throws ServletException{
	    this.initUrlContext();
	}
    
	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	    this.processRequest(request, response, RequestMethod.GET);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	    this.processRequest(request, response, RequestMethod.POST);
	}

    /**
     * @see HttpServlet#doPut(HttpServletRequest request, HttpServletResponse response)
     */
	public void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response, RequestMethod.PUT);
    }
    
    /**
     * @see HttpServlet#doDelete(HttpServletRequest request, HttpServletResponse response)
     */
	public void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response, RequestMethod.DELETE);
    }    
    
    //------------ PRIVATE METHODS ----------------
    
    private void initUrlContext() throws ServletException{
        String contextPath = this.getServletContext().getRealPath("/");
        if (logger.isDebugEnabled()){
            logger.debug("context path is " + contextPath);
        }
        try{
            FileInputStream fis = new FileInputStream(contextPath + urlmappingFile);
            urlsContext = new UrlsContext(fis);
        }
        catch(IOException e){
            throw new ServletException(e);
        }       
    }    
    
    private void processRequest(HttpServletRequest request, HttpServletResponse response, 
            RequestMethod requestMethod) throws ServletException, IOException{
        if (logger.isDebugEnabled()){
            logger.debug("Process a " + requestMethod + " request(url=" 
                    + request.getRequestURL() + ", uri="+ request.getRequestURI() +").");
        }
        String contextUri = this.getContextUri(request, requestMethod);
        
        Model model = null;
        try{
            // lookup url mapping with key(contextUri)
            Object values[] = this.lookupUrlMapping( 
                    urlsContext.getUrlMappings(), contextUri);
            UrlMapping urlMapping = (UrlMapping)values[0];
            String groups[] = (String[])values[1];
            if(logger.isDebugEnabled()){
                logger.debug("get groups(" + array2String(groups) + ") from context uri(" + contextUri + ")");
            }
            Object args[] = groups2Args(groups, urlMapping.getControllerMethod());
            
            // invoke controller method
            ControllerInvoker invoker = new WebControllerInvoker();
            model = invoker.invoke(request, response, urlMapping.getController().newInstance() ,
                    urlMapping.getControllerMethod(), args);
        }
        catch(Exception e){
            logger.error(e.getMessage(), e);
            int statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            Django4jException de = null;
            if (e instanceof Django4jException ){
                de = (Django4jException)e;
                statusCode = de.getStatusCode();
            }
            
            Throwable cause = e.getCause();
            if (cause == null) cause = e;
            String msg = "<Exception>" + cause.getMessage() + "</Exception>";
            model = new Model(msg, statusCode);
        }
        /* 当设置了response.setStatus(HttpURLConnection.HTTP_NO_CONTENT)后，即使随后
         * 调用response.getWriter()来向客户端输出数据，tomcat（在tomcat下进行的测试）不会
         * 向客户端返回任何数据，就是说这个响应只有http的头，同时也无法设置Content-Type，因为
         * NO_CONTENT就表示没有任何内容返回到客户端。 
         * 我的本意是想在找不到资源的时候，返回204（NO_CONTENT)这个响应状态码，同时在消息体中
         * 输出一些错误信息，现在看来行不同。 不知道这个是tomcat限制的，还是http协议这样限制,
         * 程序需要处理这种情况,即当找不到资源时，model.setModel(null)。
         */
        response.setContentType(model.getViewType());
//        response.setHeader("X-Content-Type", model.getViewType());
        response.setStatus(model.getStatusCode());
        if (logger.isDebugEnabled()){
            logger.debug("Got response view(statusCode=" + model.getStatusCode() 
                    + ",viewtype=" + model.getViewType() + ",model=" + model.getModel() 
                    + ") for request(" + request.getRequestURL() + ").");
        }
        
        // write response to cient
        ModelHandler handler = ModelHandlerFactory.getHandler(model.getViewType());
        if (logger.isDebugEnabled()){
            logger.debug("Got a ModelHandler(" + handler.toString() + ").");
        }
        handler.handle(model, response.getOutputStream());
        
        if (logger.isDebugEnabled()){
            logger.debug("send back response for request(" + request.getRequestURL() + ") successfully.");
        }        
    }
    
    /**
     * 从request url中获得context uri， context uri会用来与django4j-urls.properties中定义
     * 的正则表达式进行匹配。
     * @param request   http servlet request
     * @param method    controller method
     */
    private String getContextUri(HttpServletRequest request, RequestMethod method){
        String contextName = request.getSession().getServletContext().getServletContextName();
        String uri = request.getRequestURI();   // uri=/Django4j/DispatchServlet/
        String contextUri = uri.substring(("/" + contextName).length());
        contextUri += method;
        if (contextUri.startsWith(DELIM_URL)){
            contextUri = contextUri.substring(DELIM_URL.length());
        }
        if (logger.isDebugEnabled()){
            logger.debug("The context uri is '" + contextUri + "'.");
        }
        return contextUri;
    }
    
    /**
     * 根据contextUri获得对应的UrlMapping
     * @param urlmappings   url mappings represents django4j-urls.properties
     * @param contextUri    context uri
     * @return the Object[] contain 2 elements ,first is matched UrlMapping, 
     * and the second is the matched groups extract from request url, these 
     * groups will be convert to the arguments of controller method.      
     */
    private Object[] lookupUrlMapping( 
            List<UrlMapping> urlmappings, String contextUri){
        UrlMapping urlMapping = null;
        String groups[] = null;
        for (UrlMapping mapping : urlmappings){
            // mathc regular expression
            Pattern pattern = mapping.getPattern();
            Matcher matcher = pattern.matcher(contextUri);
            if (matcher.matches()){
                if (logger.isDebugEnabled()){
                    logger.debug("find url mapping(" + urlMapping 
                            + ") with contextUri(" + contextUri + ").");
                }                
                
                int groupCount = matcher.groupCount();
                groups = new String[groupCount];
                for (int i=0; i<groupCount; i++){
                    // the first group is the full parsed text.
                    groups[i] = matcher.group(i+1);
                }
                
                if (logger.isDebugEnabled()){
                    for (int i=0; i<groupCount; i++){
                        logger.debug("From + " + contextUri + " retrieve group[" 
                                + i + "]:" + groups[i]);
                    }
                }
                
                urlMapping = mapping;
                break;
            }
        }
        
        if (urlMapping == null){
            throw new Django4jException(HttpServletResponse.SC_BAD_REQUEST, 
                    "can NOT match url '" + contextUri + "'.");
        }
        return new Object[]{urlMapping, groups};
    }
    
    /**
     * convert group(String) to a typed argument for controller method
     */
    private Object[] groups2Args(String groups[],Method method) throws Exception{
        Class paraTypes[] = method.getParameterTypes();
        Object args[] = new Object[groups.length];
        for (int i=0; i<groups.length; i++){
            // the first 2 arguments of controller method should be (HttpServletRequest, HttpServletResponse)
            args[i] = TypedArgument.toType(groups[i], paraTypes[i+2]);
        }
        return args;
    }
    
    private String array2String(String args[]){
        StringBuffer buffer = new StringBuffer();
        for (String arg : args){
            buffer.append(arg).append(",");
        }
        return buffer.toString();
    }
}
